package com.yaorange.common.utils;

import com.google.zxing.*;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.client.j2se.MatrixToImageConfig;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import org.apache.commons.codec.binary.Base64;
import org.springframework.util.StringUtils;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

/**
 * ZXing工具类
 *
 * @see -----------------------------------------------------------------------------------------------------------------------
 * @see 经测试:用微信扫描GBK编码的中文二维码时出现乱码,用UTF-8编码时微信可正常识别
 * @see 并且MultiFormatWriter.encode()时若传入hints参数来指定UTF-8编码中文时,微信压根就不识别所生成的二维码
 * @see 所以这里使用的是这种方式new String(content.getBytes("UTF-8"), "ISO-8859-1")
 * @see -----------------------------------------------------------------------------------------------------------------------
 * @see 将logo图片加入二维码中间时,需注意以下几点
 * @see 1)生成二维码的纠错级别建议采用最高等级H,这样可以增加二维码的正确识别能力(我测试过,不设置级别时,二维码工具无法读取生成的二维码图片)
 * @see 2)头像大小最好不要超过二维码本身大小的1/5,而且只能放在正中间部位,这是由于二维码本身结构造成的(你就把它理解成图片水印吧)
 * @see 3)在仿照腾讯微信在二维码四周增加装饰框,那么一定要在装饰框和二维码之间留出白边,这是为了二维码可被识别
 */
public class QrCodeCreateUtil {

    private static final String CHARSET = "utf-8";
    private static final int BLACK = 0xFF000000;
    private static final int WHITE = 0xFFFFFFFF;

    private QrCodeCreateUtil() {
    }

    /**
     * 为二维码图片增加logo头像
     *
     * @param imagePath 二维码图片存放路径(含文件名)
     * @param logoPath  logo头像存放路径(含文件名)
     */
    private static File overlapImage(String imagePath, String logoPath) throws IOException {
        BufferedImage image = ImageIO.read(new File(imagePath));
        int logoWidth = image.getWidth() / 5; // 设置logo图片宽度为二维码图片的五分之一
        int logoHeight = image.getHeight() / 5; // 设置logo图片高度为二维码图片的五分之一
        int logoX = (image.getWidth() - logoWidth) / 2; // 设置logo图片的位置,这里令其居中
        int logoY = (image.getHeight() - logoHeight); // 设置logo图片的位置,这里令其居中
        Graphics2D graphics = image.createGraphics();
        graphics.drawImage(ImageIO.read(new File(logoPath)), logoX, logoY, logoWidth, logoHeight, null);
        graphics.dispose();
        ImageIO.write(image, imagePath.substring(imagePath.lastIndexOf(".") + 1), new File(imagePath));
        return new File(imagePath);
    }

    /**
     * 生成二维码
     *
     * @param content    二维码内容
     * @param charset    编码二维码内容时采用的字符集(传null时默认采用UTF-8编码)
     * @param imagePath  二维码图片存放路径(含文件名)
     * @param width      生成的二维码图片宽度
     * @param height     生成的二维码图片高度
     * @param logoPath   logo头像存放路径(含文件名,若不加logo则传null即可)
     * @param isDelWhite 是否删除白边 true/false
     * @return 生成二维码结果(true or false)
     */
    @SuppressWarnings("deprecation")
    public static boolean encodeQRCodeImage(String content, String charset, String imagePath, int width, int height,
                                            String logoPath, boolean isDelWhite) {
        Map<EncodeHintType, Object> hints = new HashMap<EncodeHintType, Object>();
        // 指定编码格式
        // hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
        // 指定纠错级别(L--7%,M--15%,Q--25%,H--30%)
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H);
        // 编码内容,编码类型(这里指定为二维码),生成图片宽度,生成图片高度,设置参数
        BitMatrix bitMatrix = null;
        try {
            bitMatrix = new MultiFormatWriter().encode(
                    new String(content.getBytes(charset == null ? "UTF-8" : charset), "ISO-8859-1"),
                    BarcodeFormat.QR_CODE, width, height, hints);
            if (isDelWhite) {
                // 删除白边
                bitMatrix = deleteWhite(bitMatrix);
            }
        } catch (Exception e) {
            System.out.println("编码待生成二维码图片的文本时发生异常,堆栈轨迹如下");
            e.printStackTrace();
            return false;
        }
        // 生成的二维码图片默认背景为白色,前景为黑色,但是在加入logo图像后会导致logo也变为黑白色,至于是什么原因还没有仔细去读它的源码
        // 所以这里对其第一个参数黑色将ZXing默认的前景色0xFF000000稍微改了一下0xFF000001,最终效果也是白色背景黑色前景的二维码,且logo颜色保持原有不变
        MatrixToImageConfig config = new MatrixToImageConfig(0xFF000001, 0xFFFFFFFF);
        // 这里要显式指定MatrixToImageConfig,否则还会按照默认处理将logo图像也变为黑白色(如果打算加logo的话,反之则不须传MatrixToImageConfig参数)
        try {
            //如果文件目录不存在则创建文件目录
            File file = new File(imagePath);
            if (!file.exists()) {
                file.mkdirs();
            }
            String format = imagePath.substring(imagePath.lastIndexOf(".") + 1);
            MatrixToImageWriter.writeToFile(bitMatrix, format, file, config);
        } catch (IOException e) {
            System.out.println("生成二维码图片[" + imagePath + "]时遇到异常,堆栈轨迹如下");
            e.printStackTrace();
            return false;
        }
        // 此时二维码图片已经生成了,只不过没有logo头像,所以接下来根据传入的logoPath参数来决定是否加logo头像
        if (null == logoPath || "".equals(logoPath)) {
            return true;
        } else {
            // 如果此时最终生成的二维码不是我们想要的,那么可以扩展MatrixToImageConfig类(反正ZXing提供了源码)
            // 扩展时可以重写其writeToFile方法,令其返回toBufferedImage()方法所生成的BufferedImage对象(尽管这种做法未必能解决为题,故需根据实际情景测试)
            // 然后替换这里overlapImage()里面的第一行BufferedImage image = ImageIO.read(new
            // File(imagePath));
            // 即private static void overlapImage(BufferedImage image, String imagePath,
            // String logoPath)
            try {
                // 这里不需要判断logoPath是否指向了一个具体的文件,因为这种情景下overlapImage会抛IO异常
                overlapImage(imagePath, logoPath);
                return true;
            } catch (IOException e) {
                System.out.println("为二维码图片[" + imagePath + "]添加logo头像[" + logoPath + "]时遇到异常,堆栈轨迹如下");
                e.printStackTrace();
                return false;
            }
        }
    }

    /**
     * 删除白边
     *
     * @param matrix
     * @return
     */
    private static BitMatrix deleteWhite(BitMatrix matrix) {
        int[] rec = matrix.getEnclosingRectangle();
        int resWidth = rec[2] + 1;
        int resHeight = rec[3] + 1;

        BitMatrix resMatrix = new BitMatrix(resWidth, resHeight);
        resMatrix.clear();
        for (int i = 0; i < resWidth; i++) {
            for (int j = 0; j < resHeight; j++) {
                if (matrix.get(i + rec[0], j + rec[1])) {
                    resMatrix.set(i, j);
                }
            }
        }
        return resMatrix;
    }

    /**
     * 解析二维码
     *
     * @param imagePath 二维码图片存放路径(含文件名)
     * @param charset   解码二维码内容时采用的字符集(传null时默认采用UTF-8编码)
     * @return 解析成功后返回二维码文本, 否则返回空字符串
     */
    public static String decodeQRCodeImage(String imagePath, String charset) {
        BufferedImage image = null;
        try {
            image = ImageIO.read(new File(imagePath));
        } catch (IOException e) {
            e.printStackTrace();
            return "";
        }
        if (null == image) {
            System.out.println("Could not decode QRCodeImage");
            return "";
        }
        BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(new BufferedImageLuminanceSource(image)));
        Map<DecodeHintType, String> hints = new HashMap<DecodeHintType, String>();
        hints.put(DecodeHintType.CHARACTER_SET, charset == null ? "UTF-8" : charset);
        Result result = null;
        try {
            result = new MultiFormatReader().decode(bitmap, hints);
            return result.getText();
        } catch (NotFoundException e) {
            System.out.println("二维码图片[" + imagePath + "]解析失败,堆栈轨迹如下");
            e.printStackTrace();
            return "";
        }
    }

    /**
     * 生成二维码
     *
     * @param uploadPath 二维码存放目录
     * @param fileName   文件名称
     * @param contents   文件内容
     * @param width      宽度
     * @param isDelWhite 是否删除白边 true/false
     * @return
     */
    public static File doCreateQrCode(String uploadPath, String fileName, String contents, Integer width, boolean isDelWhite) {
        // 创建一个临时目录
        String baseUrl = uploadPath + File.separator;
        boolean b = encodeQRCodeImage(contents, null, baseUrl + fileName + ".jpg", width, width, null, isDelWhite);
        if (b) {
            return new File(baseUrl + fileName + ".jpg");
        }
        return null;
    }

    /**
     * 根据点矩阵生成黑白图。
     *
     * @author JS
     */
    public static BufferedImage toBufferedImage(BitMatrix matrix) {
        int width = matrix.getWidth();
        int height = matrix.getHeight();
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                image.setRGB(x, y, matrix.get(x, y) ? BLACK : WHITE);
            }
        }
        return image;
    }

    /**
     * 将字符串编成一维条码的矩阵
     *
     * @param str
     * @param width
     * @param height
     * @return
     * @author JS
     */
    public static BitMatrix toBarCodeMatrix(String str, Integer width, Integer height, boolean isDelWhite) {

        if (width == null || width < 50) {
            width = 50;
        }

        if (height == null || height < 6) {
            height = 6;
        }

        try {
            // 文字编码
            Hashtable<EncodeHintType, String> hints = new Hashtable<EncodeHintType, String>();
            hints.put(EncodeHintType.CHARACTER_SET, CHARSET);
            BitMatrix bitMatrix = new MultiFormatWriter().encode(str, BarcodeFormat.CODE_128, 16, height, hints);
            if (isDelWhite) {
                // 删除白边
                bitMatrix = deleteWhite(bitMatrix);
            }
            return bitMatrix;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 根据矩阵、图片格式，生成文件。
     *
     * @author JS
     */
    public static void writeToFile(BitMatrix matrix, String format, File file) throws IOException {
        BufferedImage image = toBufferedImage(matrix);
        if (!ImageIO.write(image, format, file)) {
            throw new IOException("Could not write an image of format " + format + " to " + file);
        }
    }

    /**
     * 将矩阵写入到输出流中。
     *
     * @author JS
     */
    public static void writeToStream(BitMatrix matrix, String format, OutputStream stream) throws IOException {
        BufferedImage image = toBufferedImage(matrix);
        if (!ImageIO.write(image, format, stream)) {
            throw new IOException("Could not write an image of format " + format);
        }
    }

    /**
     * 解码，需要javase包。
     * <p>
     * <br/>
     * <br/>
     *
     * @param file
     * @return
     * @author JS
     */
    public static String decode(File file) {

        BufferedImage image;
        try {
            if (file == null || file.exists() == false) {
                throw new Exception(" File not found:" + file.getPath());
            }

            image = ImageIO.read(file);

            LuminanceSource source = new BufferedImageLuminanceSource(image);
            BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));

            Result result;

            // 解码设置编码方式为：utf-8，
            Hashtable hints = new Hashtable();
            hints.put(DecodeHintType.CHARACTER_SET, CHARSET);

            result = new MultiFormatReader().decode(bitmap, hints);

            return result.getText();

        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    /**
     * 生成条形码/二维码Base64字符串，提供给前台显示
     *
     * @param content
     * @param width
     * @param height
     * @return
     * @throws IOException
     * @Description:
     * @author JS
     * @DateTime 2019年3月18日 上午9:53:16
     */
    public static String crateQRCodeBase64(BarcodeFormat type, String content, int width, int height)
            throws IOException {
        String resultImage = "";
        if (!StringUtils.isEmpty(content)) {
            ServletOutputStream stream = null;
            ByteArrayOutputStream os = new ByteArrayOutputStream();
            @SuppressWarnings("rawtypes")
            HashMap<EncodeHintType, Comparable> hints = new HashMap<>();
            hints.put(EncodeHintType.CHARACTER_SET, "utf-8"); // 指定字符编码为“utf-8”
            hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); // 指定二维码的纠错等级为中级
            hints.put(EncodeHintType.MARGIN, 2); // 设置图片的边距
            try {
                MultiFormatWriter writer = new MultiFormatWriter();
                BitMatrix bitMatrix = writer.encode(content, type, width, height, hints);
                BufferedImage bufferedImage = MatrixToImageWriter.toBufferedImage(bitMatrix);
                ImageIO.write(bufferedImage, "png", os);
                /**
                 * 原生转码前面没有 data:image/png;base64 这些字段，返回给前端是无法被解析，可以让前端加，也可以在下面加上
                 */
                resultImage = new String("data:image/png;base64," + Base64.encodeBase64String(os.toByteArray()));
                return resultImage;
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (stream != null) {
                    stream.flush();
                    stream.close();
                }
            }
        }
        return null;
    }

    public static File doCreateBrCode(String fileName, String contents, Integer width, boolean isDelWhite) {
        // String property = System.getProperty("user.dir");
        contents = contents.replace("*", "");
        fileName = fileName.replace("*", "");
        fileName = fileName.replace("/", "_");
        File file = new File("E:" + File.separator + fileName + ".jpg");
        BitMatrix bitMatrix = toBarCodeMatrix(contents, width, 20, isDelWhite);
        try {
            writeToFile(bitMatrix, "jpg", file);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return file;
    }

    public static void main(String[] args) throws IOException {
       doCreateQrCode("D:\\work\\", "myqrcode", "hello", 400, true);
    }

}
